﻿namespace StyleCop.CSharp.JslRefactoringRules
{
    using System;
    using System.Linq;

    /// <summary>
    /// Suggests moving variables to the smallest scope they can fit into.
    /// </summary>
    public class MoveVariableDeclarationToUsageLevel : CsRuleBase<RuleAnalyzer>
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="MoveVariableDeclarationToUsageLevel"/> class.
        /// </summary>
        /// <param name="analyzer">The analyzer.</param>
        /// <param name="document">The document.</param>
        public MoveVariableDeclarationToUsageLevel(RuleAnalyzer analyzer, CsDocument document)
            : base(analyzer)
        {
            this.Document = document;
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Analyzes the graph.
        /// </summary>
        /// <param name="element">The element the graph was built from.</param>
        /// <param name="graph">The graph.</param>
        public override void AnalyzeGraph(CsElement element, CodeUnitGraph graph)
        {
            if (this.Analyzer.IsRuleEnabled(this.Document, RuleAnalyzer.MoveVariableDeclarationToUsageLevel))
            {
                this.CheckVariables(element, graph);
            }
        }
        #endregion Public Methods

        #region Private Methods
        private void CheckVariables(CsElement element, CodeUnitGraph graph)
        {
            // Get all the variable usages in the graph so we don't have to call GetVariableUsage later.
            // Key = the Literal Expression that is a variable.
            // Value = the variable usage.
            var allVariableUsages =
                (from literalExpression in graph.Vertices.OfType<LiteralExpression>()
                 let variableUsage = literalExpression.GetVariableUsage()
                 where variableUsage != null
                 select variableUsage)
                 .ToDictionary(variableUsage => variableUsage.Source);

            // Get all the unique variables used.
            var uniqueVariables = allVariableUsages
                .Values
                .Select(variableUsage => variableUsage.Variable)
                .Distinct();

            foreach (Variable variableToCheck in uniqueVariables)
            {
                // Get the variable declaration.
                VariableUsageInformation variableDeclaration = allVariableUsages
                    .Values
                    .Where(usage => usage.IsDeclaration && object.ReferenceEquals(usage.Variable, variableToCheck))
                    .FirstOrDefault();

                // If there was one found then process the variable. Parameters won't have declarations so skip them.
                if ((variableDeclaration != null)
                    && !variableDeclaration.IsCatchVariable
                    && !variableDeclaration.IsForVariable
                    && !variableDeclaration.IsUsingVariable)
                {
                    // Get all the variable usages not including the declaration.
                    var thisVariableUsages =
                        from variableUsage in allVariableUsages.Values
                        where !variableUsage.IsDeclaration
                            && object.ReferenceEquals(variableUsage.Variable, variableToCheck)
                        select variableUsage;

                    // Find the closest common ancestor of all the variable usages for this variable.
                    ICodeUnit closestCommonAncestor = null;
                    foreach (VariableUsageInformation variableUsage in thisVariableUsages)
                    {
                        if (closestCommonAncestor == null)
                        {
                            // This is the first usage to look at so by default, its the best.
                            closestCommonAncestor = variableUsage.SmallestVariableScope();
                        }
                        else
                        {
                            // Find the smallest variable scope...
                            closestCommonAncestor = variableUsage.SmallestVariableScope(closestCommonAncestor);

                            // There should always be a common ancestor since the method/constructor/whatever should
                            // be an ancestor of both.
                            if (closestCommonAncestor == null)
                            {
                                throw new InvalidOperationException("No common ancestor found.");
                            }
                        }
                    }

                    if (closestCommonAncestor != null)
                    {
                        ICodeUnit declarationContainer = variableDeclaration.SmallestVariableScope();

                        if (!object.ReferenceEquals(declarationContainer, closestCommonAncestor))
                        {
                            this.AddViolation(
                                element,
                                variableDeclaration.Source.LineNumber,
                                RuleAnalyzer.MoveVariableDeclarationToUsageLevel,
                                variableToCheck.Name,
                                closestCommonAncestor.LineNumber);
                        }
                    }
                }
            }
        }
        #endregion Private Methods
    }
}